Kalman Trend Levels [BigBeluga] — Indie Port

Table of Contents

Kalman Trend Levels [BigBeluga] — Indie Port

1. Overview & Usage Strategy

Kalman Trend Levels is a trend-following overlay indicator that uses Kalman filtering to smooth price data and detect trend transitions. This tool is designed to reduce noise while remaining responsive to directional changes. Use Case: Traders can use this indicator to:

Trading Logic

2. Formula

The Kalman Filter update in both Pine and Indie follows this structure:

prediction = estimate
kalman_gain = error_est / (error_est + error_meas)
estimate = prediction + kalman_gain * (src - prediction)
error_est = (1 - kalman_gain) * error_est + Q / length

Where:

ATR is also used:

ATR_zone = ATR(200) * 0.5

3. Key Indie Code Snippets

Kalman Filter as a Reusable Algorithm

@algorithm
def KalmanFilter(self, src: SeriesF, length: int, R: float = 0.01, Q: float = 0.1) -> SeriesF:
    estimate = MutSeriesF.new(init=float('nan'))
    ...
    estimate[0] = prediction + kalman_gain[0] * (src[0] - prediction)
    ...
    return estimate

Main Indicator Logic

@indicator("Kalman Trend Levels [BigBeluga]", overlay_main_pane=True)
class Main(MainContext):
    def calc(self, ...):
        short_kalman = KalmanFilter.new(self.close, short_len)[0]
        long_kalman = KalmanFilter.new(self.close, long_len)[0]
        trend_up = short_kalman > long_kalman
        ...
        return (
            plot.Line(short_kalman, color=trend_col1),
            plot.Line(long_kalman, color=trend_col),
            plot.Fill(color=trend_col)
        )

Candle Coloring Logic

if candle_color:
    if trend_up and short_kalman > short_kalman_2:
        candle_col = upper_col
    elif not trend_up and short_kalman < short_kalman_2:
        candle_col = lower_col
    else:
        candle_col = color.GRAY

4. Pine‑to‑Indie Comparison

Original PineScript Logic

kalman_filter(src, length, R=0.01, Q=0.1) =>
    var float estimate = na
    ...
    estimate := prediction + kalman_gain * (src - prediction)

Indie Equivalent

@algorithm
def KalmanFilter(self, src: SeriesF, length: int, R: float = 0.01, Q: float = 0.1) -> SeriesF:
    ...

Differences and Benefits

Concept PineScript Indie Equivalent Benefit
Function Scope Inline function via => Structured as @algorithm Reusable, type-safe logic
State Mgmt var state on series MutSeriesF with indexed memory Explicit control, clarity
Plotting plot, label.new, box.new @plot.line, @plot.fill, plot.* Clean syntax, modular plots
Reuse Not modular Algorithms as objects (new()) Code reuse and encapsulation
Candle Coloring Manual coloring logic Optional color=... argument Integrated, more readable

Indie emphasizes type safety, algorithm reuse, and modular design. Instead of inline, stateful logic, calculations and conditions are encapsulated in objects (MutSeriesF, Plot, Color), making Indie more suitable for complex indicators and long-term maintenance.


Let me know if you’d like this formatted into a Markdown .md file or prepared for publishing to GitHub or forums.

Indie Script implementation

#education purposes
# indie:lang_version = 5
# Copyright (c) BigBeluga, licensed under Creative Commons Attribution-NonCommercial-ShareAlike 4.0
# Ported to Indie from PineScript by Claude

from indie import indicator, MainContext, param, plot, color, MutSeriesF, algorithm, SeriesF, Optional, Color
from indie.algorithms import Atr
import math

@algorithm
def KalmanFilter(self, src: SeriesF, length: int, R: float = 0.01, Q: float = 0.1) -> SeriesF:
    estimate = MutSeriesF.new(init=float('nan'))
    error_est = MutSeriesF.new(init=1.0)
    error_meas = MutSeriesF.new(init=R * length)
    kalman_gain = MutSeriesF.new(init=0.0)
    
    if math.isnan(estimate[0]):
        estimate[0] = src[1]
    
    prediction = estimate[0]
    kalman_gain[0] = error_est[0] / (error_est[0] + error_meas[0])
    estimate[0] = prediction + kalman_gain[0] * (src[0] - prediction)
    error_est[0] = (1 - kalman_gain[0]) * error_est[0] + Q / length
    
    return estimate

@indicator("Kalman Trend Levels [BigBeluga]", overlay_main_pane=True)
@param.int("short_len", default=50, title="Short Length")
@param.int("long_len", default=150, title="Long Length")
@param.bool("retest_sig", default=False, title="Retest Signals")
@param.bool("candle_color", default=True, title="Candle Color")
@param.color("upper_col", default=color.rgba(19, 189, 110, 1.0), title="Up Color")
@param.color("lower_col", default=color.rgba(175, 13, 75, 1.0), title="Down Color")
@plot.line("p1", title="Short Kalman")
@plot.line("p2", title="Long Kalman", line_width=2)
@plot.fill("p1", "p2")
class Main(MainContext):
    def __init__(self):
        self.prev_short_kalman = 0.0
        self.prev_long_kalman = 0.0
        self.prev_trend_up = False
    
    def calc(self, short_len, long_len, retest_sig, candle_color, upper_col, lower_col):
        atr = Atr.new(200, "RMA")[0] * 0.5
        
        short_kalman_series = KalmanFilter.new(self.close, short_len)
        long_kalman_series = KalmanFilter.new(self.close, long_len)
        
        short_kalman = short_kalman_series[0]
        long_kalman = long_kalman_series[0]
        
        short_kalman_2 = self.prev_short_kalman
        self.prev_short_kalman = short_kalman
        
        trend_up = short_kalman > long_kalman
        prev_trend_up = self.prev_trend_up
        self.prev_trend_up = trend_up
        
        trend_col = upper_col if trend_up else lower_col
        trend_col1 = upper_col if short_kalman > short_kalman_2 else lower_col
        
        candle_col: Optional[Color] = None
        if candle_color:
            if trend_up and short_kalman > short_kalman_2:
                candle_col = upper_col
            elif not trend_up and short_kalman < short_kalman_2:
                candle_col = lower_col
            else:
                candle_col = color.GRAY
        
        close_rounded = math.floor(self.close[0] * 10) / 10
        
        if trend_up and not prev_trend_up:
            up_label_text = "\\u2B09\\n" + str(close_rounded)
            pass
        
        if trend_up == prev_trend_up:
            pass
        
        if prev_trend_up and not trend_up:
            down_label_text = str(close_rounded) + "\\n\\u2B83"
            pass
        
        return (
            plot.Line(short_kalman, color=trend_col1),
            plot.Line(long_kalman, color=trend_col),
            plot.Fill(color=trend_col)
        )


Pine Script implementation

//education purposes
// This work is licensed under Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International  
// https://creativecommons.org/licenses/by-nc-sa/4.0/
// © BigBeluga

//@version=5
indicator("Kalman Trend Levels [BigBeluga]", overlay = true, max_labels_count = 500, max_boxes_count = 500)

// INPUTS ――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――{
int short_len     = input.int(50)
int long_len      = input.int(150)
bool retest_sig   = input.bool(false, "Retest Signals")
bool candle_color = input.bool(true, "Candle Color")

color upper_col = input.color(#13bd6e, "up", inline = "colors")
color lower_col = input.color(#af0d4b, "dn", inline = "colors")

// }


// CALCULATIONS――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――{
float atr = ta.atr(200) *0.5

var lower_box = box(na)
var upper_box = box(na)


// Kalman filter function
kalman_filter(src, length, R = 0.01, Q = 0.1) =>
    // Initialize variables
    var float estimate = na
    var float error_est = 1.0
    var float error_meas = R * (length)
    var float kalman_gain = 0.0
    var float prediction = na
    
    // Initialize the estimate with the first value of the source
    if na(estimate)
        estimate := src[1]

    // Prediction step
    prediction := estimate
    
    // Update Kalman gain
    kalman_gain := error_est / (error_est + error_meas)

    // Update estimate with measurement correction
    estimate := prediction + kalman_gain * (src - prediction)

    // Update error estimates
    error_est := (1 - kalman_gain) * error_est  + Q / (length) // Adjust process noise based on length

    estimate

float short_kalman = kalman_filter(close, short_len)
float long_kalman = kalman_filter(close, long_len)

bool trend_up = short_kalman > long_kalman

color trend_col  = trend_up ? upper_col : lower_col
color trend_col1 = short_kalman > short_kalman[2] ? upper_col : lower_col
color candle_col = candle_color ? (trend_up and short_kalman > short_kalman[2] ? upper_col : not trend_up and short_kalman < short_kalman[2] ? lower_col : color.gray) : na

// }


// PLOT ――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――{
if trend_up and not trend_up[1]
    label.new(bar_index, short_kalman, "🡹\n" + str.tostring(math.round(close,1)), color = color(na), textcolor = upper_col, style = label.style_label_up, size = size.normal)
    lower_box := box.new(bar_index, low+atr, bar_index, low, border_color = na, bgcolor = color.new(upper_col, 60))

if not ta.change(trend_up)
    lower_box.set_right(bar_index)

if trend_up[1] and not trend_up
    label.new(bar_index, short_kalman, str.tostring(math.round(close,1))+"\n🢃", color = color(na), textcolor = lower_col, style = label.style_label_down, size = size.normal)
    upper_box := box.new(bar_index, high, bar_index, high-atr, border_color = na, bgcolor = color.new(lower_col, 60))

if not ta.change(trend_up)
    upper_box.set_right(bar_index)

if retest_sig
    if high < upper_box.get_bottom() and high[1]>= upper_box.get_bottom() //or high < lower_box.get_bottom() and high[1]>= lower_box.get_bottom()
        label.new(bar_index-1, high[1], "x", color = color(na), textcolor = lower_col, style = label.style_label_down, size = size.normal)

    if low > lower_box.get_top() and low[1]<= lower_box.get_top()
        label.new(bar_index-1, low[1], "+", color = color(na), textcolor = upper_col, style = label.style_label_up, size = size.normal)

p1 = plot(short_kalman, "Short Kalman", color = trend_col1)
p2 = plot(long_kalman, "Long Kalman", linewidth = 2, color = trend_col)

fill(p1, p2, short_kalman, long_kalman, na, color.new(trend_col, 80))

plotcandle(open, high, low, close, title='Title', color = candle_col, wickcolor=candle_col, bordercolor = candle_col)
// }